package harvard.cscie160.hw7.project;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashSet;


/**
 * 
 * @author M. Bret Blackford
 *
 */
public class ATMImpl extends UnicastRemoteObject implements ATM {
	
	private BankInterface bank;
	private SecurityInterface security;
	private float atmCashBalance;
	
	//HashSet maintains a unique list
	private HashSet<ATMListener> listeners;
	private HashSet<Integer> authenticatedAccounts;


	public ATMImpl(BankInterface bank) throws RemoteException {
		
		this.bank = bank;
		security = bank.getSecurity();
		atmCashBalance = 500; //default per requirements
		authenticatedAccounts = new HashSet<Integer>();
		listeners = new HashSet<ATMListener>();
	}



	private AccountInterface getAccount(AccountInfo accountInfo)
			throws RemoteException {
		
		AccountInterface account = bank.getAccount(accountInfo
				.getAccountNumber());
		
		if (account == null) {
			throw new ATMException("ERROR bad account ID :" + accountInfo.getAccountNumber());
		}
		return account;
	}

	public void deposit(AccountInfo accountInfo, float amount) throws RemoteException {
		
		checkAccessPermissions(accountInfo.getAccountNumber(), OperationEnumType.DEPOSIT);

		if (amount <= 0) {
			throw new ATMException("Error: amount must be positive");
		}

		AccountInterface account = getAccount(accountInfo);
		
		if (account == null) {
			throw new ATMException(" ERROR with ID" + accountInfo.getAccountNumber() );
		}

		notifyListeners("Deposit $" + amount + " into account " + accountInfo.getAccountNumber());
		float balance = account.getAccountBalance();
		account.setAccountBalance(balance + amount);
	}

	
	public void withdraw(AccountInfo accountInfo, float amount)
			throws RemoteException {
		
		int accountNumber = accountInfo.getAccountNumber();
		Integer PIN = accountInfo.getPIN();
		String pinString = PIN.toString();
		
		//checkAccessPermissions(accountNumber, OperationEnumType.WITHDRAWAL);
		checkAccessPermissions(accountInfo, OperationEnumType.WITHDRAWAL);
		
		if (amount <= 0) {
			throw new ATMException("ERROR: withdrawn amount must be positive");
		}

		if (atmCashBalance - amount < 0) {
			throw new ATMException("can an ATM run out of $$$ ?");
		}

		AccountInterface account = getAccount(accountInfo);
		if (account == null) {
			throw new ATMException(" ERROR with ID " + accountNumber);
		}

		float newAccountBalance = account.getAccountBalance() - amount;
		
		//although error sent may not correct state (account balance)
		if (newAccountBalance < 0) {
			throw new ATMException("Insufficient funds");
		}

		notifyListeners("Withdraw $" + amount + " from account "
				+ accountNumber);
		
		account.setAccountBalance(newAccountBalance);
		atmCashBalance -= amount;
	}

	public Float getBalance(AccountInfo accountInfo) throws RemoteException {
		
		checkAccessPermissions(accountInfo.getAccountNumber(), OperationEnumType.BALANCE);
		AccountInterface account = getAccount(accountInfo);
		
		return account.getAccountBalance();
	}

	

	public synchronized void transfer(AccountInfo fromAccount,
			AccountInfo toAccount, float amount) throws RemoteException {

		checkAccessPermissions(fromAccount.getAccountNumber(), OperationEnumType.WITHDRAWAL);
		checkAccessPermissions(toAccount.getAccountNumber(), OperationEnumType.DEPOSIT);
		
		//checkAccessPermissions(fromAccount, OperationEnumType.WITHDRAWAL);
		//checkAccessPermissions(toAccount, OperationEnumType.DEPOSIT);
		
		if (amount <= 0) {
			throw new ATMException("Error: amount must be positive");
		}

		AccountInterface accountFrom = getAccount(fromAccount);
		AccountInterface accountTo = getAccount(toAccount);

		//NOTE: not expected to prevent inconsistent results due to overlapping
		//requests by different ATMs.
		synchronized (accountFrom) {
			synchronized (accountTo) {
				float balanceFrom = accountFrom.getAccountBalance();
				float balanceTo = accountTo.getAccountBalance();

				if (balanceFrom < amount){
					throw new ATMException("Insufficient funds");}

				notifyListeners(" transfer $" + amount + " from account "
						+ fromAccount.getAccountNumber() + " to account " + toAccount.getAccountNumber());
				
				//let's make the transfer
				accountFrom.setAccountBalance(balanceFrom - amount);
				accountTo.setAccountBalance(balanceTo + amount);
			}
		}
	}

	public void addListener(ATMListener listener) throws RemoteException {
		listeners.add(listener);

	}

	private void notifyListeners(String message) throws RemoteException {
		
		TransactionNotification notification = new TransactionNotification(
				message);
		
		for (ATMListener listener : listeners) {
			listener.sendTransactionNotification(notification);
		}
	}


	public void removeListener(ATMListener listener) throws RemoteException {
		listeners.remove(listener);
	}

	private void checkAccessPermissions(int accountNumber,
			OperationEnumType type) throws RemoteException {

		if (!authenticatedAccounts.contains(accountNumber)) {
			throw new ATMException("ID " + accountNumber
					+ " could not be authenticated");
		}
		if (!security.checkSecurityAccess(accountNumber, type)) {
			throw new ATMException(type + " not permitted for account :"
					+ accountNumber);
		}
	}
	
	private void checkAccessPermissions(AccountInfo accountInfo,
			OperationEnumType type) throws RemoteException {

		int accountNumber = accountInfo.getAccountNumber();
		Integer pinString = accountInfo.getPIN();

		if (!authenticatedAccounts.contains(accountNumber)) {
			throw new ATMException("ID " + accountNumber
					+ " could not be authenticated");
		}

		if (!security.checkSecurityAccess(accountNumber, type)) {
			throw new ATMException(type + " not permitted for account :"
					+ accountNumber);
		}

/*		if (!security.checkPIN(accountNumber, pinString.intValue())) {
			throw new ATMException("PIN not correct for account :"
					+ accountNumber);
		}*/
	}


	public void authenticate(int accountNumber, String PIN)
			throws RemoteException {
		
		if (!security.checkPIN(accountNumber, PIN)) {
			throw new ATMException("ID " + accountNumber
					+ " could not be authenticated");
		}
		
		authenticatedAccounts.add(accountNumber);
		notifyListeners("ID:[" + accountNumber + "] authenticated");
	}

	public void authenticate(AccountInfo accountIfo) throws RemoteException {
		
		Integer tempint = accountIfo.getPIN();
		String pinString =  tempint.toString();
		
		if (!security.checkPIN(accountIfo.getAccountNumber(), pinString)) {
			throw new ATMException(" bad ID/PIN " + accountIfo.getAccountNumber());
		}
		
		authenticatedAccounts.add(accountIfo.getAccountNumber());
		notifyListeners("ID:[" + accountIfo.getAccountNumber() + "] authenticated");	
	}

	public void endSession() throws RemoteException {
		notifyListeners("ending ATM session");
		authenticatedAccounts = new HashSet<Integer>();
		listeners = new HashSet<ATMListener>();
	}

	public void setDefaults() throws RemoteException {
		bank.resetDeafaults();
	}
}
